Skip to content

rx.memo fixes to retain more API surface from the previous implementation#6598

Merged
masenf merged 13 commits into
mainfrom
masenf/move-memo-fixups
Jun 3, 2026
Merged

rx.memo fixes to retain more API surface from the previous implementation#6598
masenf merged 13 commits into
mainfrom
masenf/move-memo-fixups

Conversation

@masenf
Copy link
Copy Markdown
Collaborator

@masenf masenf commented Jun 3, 2026

  • allow bare annotations with warning -- treat as rx.Var wrapped
  • allow no return annotation -- treat as rx.Component
  • defer evaluation of memo functions to use/compile time
    • removed return annotation suggestion feature to reduce complexity; -> rx.Component will suffice
  • add EMPTY_VAR_COMPONENT so that children args can have a default and don't need to be passed.

What is still outstanding?

  • In some instances key, id, and/or other base Component props were accepted on the legacy rx.memo CustomComponent class. Now these require the RestProp. If there was a way to at least keep key working without a lot of hacks, that would be nice, since we have quite a bit of code in the wild that is using memo components with rx.foreach and need to be able to specify the react key. Obviously these can and should be rewritten to use RestProp, but I wonder if there's a way we can keep them working and throw up a deprecation warning?

masenf and others added 3 commits June 2, 2026 14:11
…warning

Older @rx.memo code (the old custom_component) annotated parameters with
bare Python types (e.g. `name: str`) and omitted return annotations. The
new rx.memo required rx.Var[...]-typed params and hard-errored otherwise,
breaking that code.

Coerce bare-type params into rx.Var[...] (children -> rx.Var[rx.Component])
in the public decorator path and flag them so the existing single
deprecation warning points the user at the parameters and return type that
still need explicit annotations. Strict-mode internal callers keep raising.

Adds a deprecation news fragment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The decorator no longer runs the decorated function body at import time.
memo() now does only signature-level work (return-annotation check, param
analysis, deprecation warning, registration) and stores a placeholder body
with _evaluated=False. The body is compiled lazily on first access via
_ensure_definition_evaluated, triggered by the component wrapper at
instantiation or by the compiler (get_component/get_function).

This speeds up import and lets a memo reference modules that aren't fully
imported yet, sidestepping circular-import ordering issues during
decoration. Body-dependent validation (hooks, non-bundled imports, "must
return a component") now surfaces at first use/compile instead of import.

Drop _bind_self_reference: recursion now resolves naturally because the
decorated name is bound by first use, and the _evaluating re-entrancy guard
covers in-eval recursion. The missing-return deprecation hint is now a
constant `-> rx.Component`, removing the body-eval-dependent return-type
inference (and its namespace-resolution helpers) so the warning stays eager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an empty-component `rx.Var[rx.Component]` sentinel, the component
counterpart to EMPTY_VAR_STR / EMPTY_VAR_INT, for use as a default on
`@rx.memo` `children` slots and other `rx.Var[rx.Component]` props.

It is defined in `reflex_base.components.memo` rather than `component.py`:
materializing a component var eagerly imports `Bare` (and thus
`reflex_base.environment`), so defining it in the always-early `component.py`
cycles when `environment` is the import entry point. `memo.py` is imported
lazily, after `environment` is ready. Exposed as `rx.EMPTY_VAR_COMPONENT`,
documented in the memo docs, and used by the internal skeleton component.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@masenf masenf requested review from a team and Alek99 as code owners June 3, 2026 04:32
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jun 3, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing masenf/move-memo-fixups (3f9259a) with main (17e3392)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR extends @rx.memo with three capabilities: lazy body evaluation (deferred to first use / compile time instead of import time), backward-compatible coercion of bare Python-type annotations to rx.Var[...] with a deprecation warning, and a new rx.EMPTY_VAR_COMPONENT sentinel for optional children slots.

  • Lazy evaluationmemo() no longer runs the decorated body; _ensure_definition_evaluated triggers evaluation on first get_component() / get_function() call, guarded by _evaluated / _evaluating flags mutated via object.__setattr__ on frozen dataclasses. Errors that previously surfaced at decoration time (bad return type, hook-bearing vars, non-bundled imports) now surface at compile time.
  • Bare-annotation coercion_analyze_params now detects non-Var / non-EventHandler annotations in public memo() calls and wraps them in Var[...], emitting a single deprecation warning so legacy memos keep working until 1.0.
  • EMPTY_VAR_COMPONENT — defined in memo.py (not component.py) to avoid the circular import with reflex_base.environment, and exported as rx.EMPTY_VAR_COMPONENT; used immediately in skeleton_component for an optional children slot.

Confidence Score: 4/5

Safe to merge; the lazy-evaluation path is well-guarded and the test suite covers recursive memos, bare-type coercion, EMPTY_VAR_COMPONENT, and the deferred-error semantics for hook/import validation.

The only concerns are a non-atomic check-and-set of the _evaluating flag (benign in Reflex's single-threaded component creation but fragile for threaded callers) and an implicit contract on EMPTY_VAR_COMPONENT being used only for Var[Component]-typed parameters. Removal of _bind_self_reference is covered by the existing test_self_referencing_component_memo test which exercises the full compile path.

packages/reflex-base/src/reflex_base/components/memo.py — specifically _ensure_definition_evaluated and the new _evaluating flag mutation under concurrent access.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/components/memo.py Core change: adds lazy evaluation via _ensure_definition_evaluated, drops _bind_self_reference, adds EMPTY_VAR_COMPONENT, and adds backward-compat coercion of bare-type annotations. The reentrancy guard is correct for module-level and closure-scoped self-referencing memos. Non-atomic check-and-set of _evaluating is a theoretical race condition but benign in Reflex's typical single-threaded component creation context.
tests/units/components/test_memo.py Well-updated test suite: existing tests migrated from .component/.function direct access to .get_component()/.get_function(), new tests for lazy-eval behavior, bare-type coercion, EMPTY_VAR_COMPONENT, and hook/import validation now exercised via explicit compiler calls. test_self_referencing_component_memo (unchanged, outside the diff) continues to cover recursive component memos end-to-end.
packages/reflex-components-internal/src/reflex_components_internal/components/base/skeleton.py Adds optional children slot to skeleton_component using EMPTY_VAR_COMPONENT as the default, allowing the skeleton to optionally wrap child content.
reflex/init.py Adds EMPTY_VAR_COMPONENT to the lazy-loader mapping so it is accessible as rx.EMPTY_VAR_COMPONENT.
reflex/compiler/utils.py Compiler updated to use definition.get_component() and definition.get_function() instead of direct field access, correctly triggering lazy evaluation at compile time.

Reviews (1): Last reviewed commit: "feat(memo): add rx.EMPTY_VAR_COMPONENT d..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/components/memo.py Outdated
Comment thread packages/reflex-base/src/reflex_base/components/memo.py
FarhanAliRaza and others added 7 commits June 3, 2026 16:12
The legacy custom-component @rx.memo accepted base Component props
directly on the wrapper — notably `key`, needed when memos are used
under rx.foreach. A RestProp-less memo now forwards key/id/class_name/
style/custom_attrs/ref to the rendered component (with a deprecation
warning pointing at rx.RestProp) instead of raising. Identity/internal
fields (tag, library, event_triggers, ...) stay rejected since
overriding them would corrupt the render.
Replace the per-definition _evaluated/_evaluating flags and the shared
_ensure_definition_evaluated routine with a dedicated _LazyBody generic
that owns deferral, caching, and the re-entrancy guard. MemoDefinition
now exposes `.component` / `.function` as plain properties instead of
get_component() / get_function() methods, so callers read a value rather
than invoke an evaluation step. A placeholder-less re-entrant read now
raises instead of silently returning a stale body, surfacing broken
invariants loudly.
Python <=3.10's get_type_hints rewrites a `param: X = None` annotation
into Optional[X], hiding the real type from the param classifiers — so
an EventHandler param with a None default slipped past validation on
3.10 instead of being rejected. Strip the Optional wrapper before
classifying, gated behind a version check so 3.11+ skips the extra
get_origin work it doesn't need.
The base-prop passthrough deprecation lives in reflex-base, so its news
fragment belongs alongside that package rather than in the repo-root
news directory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FarhanAliRaza
Copy link
Copy Markdown
Contributor

FarhanAliRaza commented Jun 3, 2026

The component properties I allowed into memos maybe no op, have to test those. Key works that we need the most i think.
Edit: Only added key supprt instead of forwarding all props.

FarhanAliRaza and others added 3 commits June 4, 2026 01:18
…ed props

Other base props (id, class_name, style, custom_attrs, ref) never reach
the rendered root without a `...rest` spread, so accepting them just
dropped them silently. Reject them with a message pointing at
`rx.RestProp`; `key` stays forwardable since React reads it at the
reconciliation layer. Gate the deprecation warning per-wrapper so a
keyed memo under `rx.foreach` doesn't re-walk the call stack per row.
allow RestProp values to be updated with other dict/objectvar values and still
conveniently function as a RestProp.
@masenf masenf merged commit baf10e6 into main Jun 3, 2026
107 checks passed
@masenf masenf deleted the masenf/move-memo-fixups branch June 3, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants